/**
*    This file is part of Basic Service.
*
*    Basic Service is free software: you can redistribute it and/or modify
*    it under the terms of the GNU General Public License as published by
*    the Free Software Foundation, either version 3 of the License, or
*    (at your option) any later version.
*
*    Basic Service is distributed in the hope that it will be useful,
*    but WITHOUT ANY WARRANTY; without even the implied warranty of
*    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
*    GNU General Public License for more details.
*
*    You should have received a copy of the GNU General Public License
*    along with Basic Service (See GPL.txt).  If not, see <http://www.gnu.org/licenses/>.
*    
* 	If needed the author is Amir Zucker and can be reached at amirzucker1@gmail.com or amirzucker.wordpress.com
*/
package com.basicservice.controller;

import java.io.IOException;
import java.net.URI;
import java.util.Date;
import java.util.Locale;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.owasp.appsensor.errors.AppSensorException;
import org.owasp.esapi.ESAPI;
import org.owasp.esapi.errors.EncryptionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.util.UriTemplate;

import com.basicservice.domain.ActionValidation;
import com.basicservice.domain.ActionValidation.TYPE;
import com.basicservice.domain.User;
import com.basicservice.dto.validators.EmailValidatedString;
import com.basicservice.dto.validators.PasswordValidatedString;
import com.basicservice.dto.validators.UUIDValidatedString;
import com.basicservice.repository.UserRepository;
import com.basicservice.service.ActionValidationService;
import com.basicservice.service.Constants;
import com.basicservice.service.GeneralService;
import com.basicservice.service.Utils;
import com.basicservice.service.exceptions.UserRegistrationException;

/**
 * User controller, manages login/register and other user actions
 */

@Controller
@RequestMapping("/api/users")
public class UserController extends ApiController {
	private static final Logger LOG = LoggerFactory.getLogger(UserController.class);

	@Autowired
	private GeneralService generalService;
	@Autowired
	private ActionValidationService actionValidationService;

	@RequestMapping(method=RequestMethod.GET)
	@ResponseBody
	public User findLoggedinUser(
			HttpServletRequest request,
			HttpServletResponse response) {
		User user = getUserFromSession(request);
		if (user != null) {
			return user;
		} else {
			response.setStatus(HttpServletResponse.SC_NOT_FOUND);
			return null;
		}
	}

	@RequestMapping(value="/logout", method=RequestMethod.POST)
	public ResponseEntity<String> logout(
			HttpServletRequest request,
			HttpServletResponse response) {
		User user = getUserFromSession(request);
		final HttpHeaders headers = new HttpHeaders();
		Cookie cookie = new Cookie(Constants.AUTHENTICATED_SESSION_ID_COOKIE, "");
		cookie.setPath("/");
		cookie.setMaxAge(0);
		response.addCookie(cookie);
		if (user != null) {
			return new ResponseEntity<String>(headers, HttpStatus.OK);
		} else {
			return new ResponseEntity<String>(headers, HttpStatus.NOT_FOUND);
		}
	}
			
	@RequestMapping(value="/login", method=RequestMethod.POST)
	public ResponseEntity<String> login(HttpServletRequest request, 
			@RequestParam(value="email") EmailValidatedString email, 
			@RequestParam(value="password") PasswordValidatedString password
			) {
			if (email == null || email.getValue() == null || password == null || password.getValue() == null) {
				return new ResponseEntity<String>(HttpStatus.NOT_FOUND);
			}
			String hashedPassword;
			try {
				hashedPassword = encryptionService.hash(password.getValue(), email.getValue());
			} catch (EncryptionException e) {
				LOG.error("Failed hashing password!!!!  This should NOT happen!");
				return new ResponseEntity<String>(HttpStatus.INTERNAL_SERVER_ERROR);
			}
			User user = userService.login(email.getValue(), hashedPassword);
			LOG.debug("Got login request: email:" + email.getValue() + ", pass:" + password);
			
			if (user == null) { 
				try {
					new AppSensorException("AF1", "Authentication Failure", "Authentication Failure detected");
				} catch (Exception e) {
					// AppSensor might throw an exception, but we want to catch it here and stop propagation
				}
				return new ResponseEntity<String>(HttpStatus.NOT_FOUND);
			} else 
			if (! user.isEmailConfirmed()) {
				// let the user know that she can't login until she confirms the email
				return new ResponseEntity<String>(HttpStatus.FORBIDDEN);
			}

			user.setLastLogin(new Date());

			LOG.debug("Preparing to save user to db");
			userService.save(user);
			final HttpHeaders headers = new HttpHeaders();
			headers.add("Set-Cookie", Constants.AUTHENTICATED_SESSION_ID_COOKIE + "=" + user.getSessionId() + "; Path=/");
			return new ResponseEntity<String>(headers, HttpStatus.OK);
	}	
	
	@RequestMapping(value="/resendConfirmationEmail", method=RequestMethod.POST)
	public ResponseEntity<String> resendConfirmationEmail(HttpServletRequest request, 
			@RequestParam(value="email") EmailValidatedString email 
			) {
		if (email == null || email.getValue() == null) {
			return new ResponseEntity<String>(HttpStatus.NOT_FOUND);
		}
		User user = userService.getByEmail(email.getValue());
		LOG.debug("Got resendConfirmationEmail request: email:" + email.getValue());
		
		if (user == null) { 
			return new ResponseEntity<String>(HttpStatus.NOT_FOUND);
		}
		
		if (user.isEmailConfirmed()) { // email already confirmed 
			return new ResponseEntity<String>(HttpStatus.PRECONDITION_FAILED);
		}
		
		LOG.debug("Preparing to resend confirmation email");

		sendConfirmationEmailToUser(request, email, user);
		
		return new ResponseEntity<String>(HttpStatus.OK);
	}	
	
	
	@RequestMapping(value="/sendResetPasswordEmail", method=RequestMethod.POST)
	public ResponseEntity<String> sendResetPasswordEmail(HttpServletRequest request, 
			@RequestParam(value="email") EmailValidatedString email 
			) {
		if (email == null || email.getValue() == null) {
			return new ResponseEntity<String>(HttpStatus.NOT_FOUND);
		}
		User user = userService.getByEmail(email.getValue());
		LOG.debug("Got resetPassword request: email:" + email.getValue());
		
		if (user == null) { 
			return new ResponseEntity<String>(HttpStatus.NOT_FOUND);
		}
		
		LOG.debug("Preparing to send resetPassword email");
		
		sendActionEmailToUser(request, email, user, 
				Constants.MLS_PASSWORD_RESET_SUBJECT, 
				Constants.MLS_PASSWORD_RESET_HTML,
				ActionValidation.TYPE.PASSWORD_RESET, 
				"{requestUrl}admin/?action=passwordReset&key={key}");
		
		return new ResponseEntity<String>(HttpStatus.OK);
	}	
	
	@RequestMapping(value="/register", method=RequestMethod.POST)
	public ResponseEntity<String> register(HttpServletRequest request, 
			@RequestParam(value="name") String name,
			@RequestParam(value="email") EmailValidatedString email, 
			@RequestParam(value="password") PasswordValidatedString password
			) {
		if (email == null || email.getValue() == null || password == null || password.getValue() == null) {
			return new ResponseEntity<String>(HttpStatus.FORBIDDEN);
		}
		LOG.debug("Got register request: email:" + email.getValue() + ", pass:" + password + ", name:" + name);
		
		User user = null;
		String hashedPassword;
		try {
			try {
				hashedPassword = encryptionService.hash(password.getValue(), email.getValue());
			} catch (EncryptionException e) {
				LOG.error("Failed hashing password!!!!  This should NOT happen!");
				return new ResponseEntity<String>(HttpStatus.INTERNAL_SERVER_ERROR);
			}
			user = userService.register(name, email.getValue(), hashedPassword);
		} catch (UserRegistrationException e) {
			if (e.getReason() == UserRegistrationException.reason.EMAIL_EXISTS) {
				return new ResponseEntity<String>(HttpStatus.FORBIDDEN);
			} else
			if (e.getReason() == UserRegistrationException.reason.INVALID_FIELD) {
				return new ResponseEntity<String>(HttpStatus.INTERNAL_SERVER_ERROR);
			}
		}
		if (user == null) { 
			return new ResponseEntity<String>(HttpStatus.INTERNAL_SERVER_ERROR);
		}

		final String id = user.getId();
		if (id != null) {
			sendConfirmationEmailToUser(request, email, user);
		}
		return new ResponseEntity<String>(HttpStatus.CREATED);
	}

	private void sendConfirmationEmailToUser(HttpServletRequest request,
			EmailValidatedString email, User user) {
		sendActionEmailToUser(request, email, user, 
				Constants.MLS_REGISTRATION_CONFIRMATION_SUBJECT, 
				Constants.MLS_REGISTRATION_CONFIRMATION_HTML,
				ActionValidation.TYPE.EMAIL_VALIDATION, 
				"{requestUrl}api/users/registrationConfirmation/?key={key}");
	}

	/**
	 * @param request
	 * @param email
	 * @param user
	 * @param subjectText 
	 * @param bodyContent 
	 * @param action 
	 */
	public void sendActionEmailToUser(HttpServletRequest request,
			EmailValidatedString email, User user, String subjectText, 
			String bodyContent, TYPE actionType, String actionPath) {
		String from = Constants.SUPPORT_BASICSERVICE;
		String to = email.getValue();
		Locale locale = LocaleContextHolder.getLocale();
		String subject = messageLocalizationService.getMessage(subjectText, locale);

		String path = Utils.getBasicPath(request);
		ActionValidation av = actionValidationService.generateActionValidation(user.getId(), actionType);
		URI uri = new UriTemplate(actionPath).expand(path, av.getKey());
		String action = uri.toASCIIString();
		Object[] args = new String[1];
		args[0] = action;
		LOG.info("Sending "+action+" link: " + args[0]);
		String messageHtml = messageLocalizationService.getMessage(bodyContent, args, locale);
		try {
			mailService.sendEmail(from, to, subject, messageHtml);
		} catch (Exception e) {
			LOG.debug("Failed to send email to: " + to, e);
		}
	}	

	@RequestMapping(value="/registrationConfirmation", method=RequestMethod.GET)
	public ResponseEntity<String> confirmEmail(
			HttpServletRequest request, 
			@RequestParam(value="key") UUIDValidatedString key,
			HttpServletResponse response
			) {
		String uri;
		ResponseEntity<String> responseEntity;

		String validatedKey = Utils.getValueFromValidatedString(key);
		if (validatedKey == null) { // key not validated
			uri = Constants.ADMIN_EMAIL_CONFIRMATION_FAILURE_REDIRECT;
			responseEntity = new ResponseEntity<String>(HttpStatus.NOT_FOUND);
		} else {
			ActionValidation actionValidation = actionValidationService.getActionValidationByKey(validatedKey);
			if (actionValidation == null || !actionValidation.isValid(ActionValidation.TYPE.EMAIL_VALIDATION)) {
				uri = Constants.ADMIN_EMAIL_CONFIRMATION_FAILURE_REDIRECT;
				responseEntity = new ResponseEntity<String>(HttpStatus.NOT_FOUND);
			} else {
				User user = userService.get(actionValidation.getUserId());
				user.setEmailConfirmed(true);
				userService.save(user);
				uri = Constants.ADMIN_EMAIL_CONFIRMATION_SUCCESS_REDIRECT;
				responseEntity = new ResponseEntity<String>(HttpStatus.OK);
			}
		}
		try {
			response.sendRedirect(response.encodeRedirectURL(uri));
		} catch (IOException e) {
		}
		return responseEntity;		
	}	
	
	@RequestMapping(value="/resetPassword", method=RequestMethod.POST)
	public ResponseEntity<String> resetPassword(
			HttpServletRequest request, 
			@RequestParam(value="key") UUIDValidatedString key,
			@RequestParam(value="email") EmailValidatedString email,
			@RequestParam(value="newPassword") PasswordValidatedString newPassword,
			@RequestParam(value="confirmPassword") PasswordValidatedString confirmPassword,
			HttpServletResponse response
			) {
		String validatedKey = Utils.getValueFromValidatedString(key);
		if (validatedKey == null) { // key not validated
			return new ResponseEntity<String>(HttpStatus.NOT_FOUND);
		}
		ActionValidation actionValidation = actionValidationService.getActionValidationByKey(validatedKey);
		if (actionValidation == null || !actionValidation.isValid(ActionValidation.TYPE.PASSWORD_RESET)) {
			return new ResponseEntity<String>(HttpStatus.NOT_FOUND);
		}
		
		if (newPassword == null || newPassword.getValue() == null || confirmPassword == null || confirmPassword.getValue() == null) {
			return new ResponseEntity<String>(HttpStatus.NOT_FOUND);
		}
		
		User user = userService.get(actionValidation.getUserId());
		return doPasswordChange(email.getValue(), newPassword.getValue(), confirmPassword.getValue(), user);		
	}
	
	@RequestMapping(value="/changePassword", method=RequestMethod.POST)
	public ResponseEntity<String> changePassword(
			HttpServletRequest request, 
			@RequestParam(value="email") EmailValidatedString email,
			@RequestParam(value="currentPassword") PasswordValidatedString currentPassword,
			@RequestParam(value="newPassword") PasswordValidatedString newPassword,
			@RequestParam(value="confirmPassword") PasswordValidatedString confirmPassword,
			HttpServletResponse response
			) {
		User user = userService.getByEmail(email.getValue());
		if (user == null) {  // user not found by key
			return new ResponseEntity<String>(HttpStatus.NOT_FOUND);
		}
		String hashedPassword;
		try {
			hashedPassword = encryptionService.hash(currentPassword.getValue(), email.getValue());
		} catch (EncryptionException e) {
			LOG.error("Failed hashing password!!!!  This should NOT happen!");
			return new ResponseEntity<String>(HttpStatus.INTERNAL_SERVER_ERROR);
		}
		if (! hashedPassword.equals(user.getPassword())) {
			return new ResponseEntity<String>(HttpStatus.NOT_FOUND);
		}
		if (newPassword == null || newPassword.getValue() == null || confirmPassword == null || confirmPassword.getValue() == null) {
			return new ResponseEntity<String>(HttpStatus.NOT_FOUND);
		}
		return doPasswordChange(email.getValue(), newPassword.getValue(), confirmPassword.getValue(), user);		
	}

	/**
	 * @param email
	 * @param password
	 * @param confirmPassword
	 * @param userId
	 * @return
	 */
	public ResponseEntity<String> doPasswordChange(String email,
			String password, String confirmPassword, User user) {
		if (password == null || !password.equals(confirmPassword)) { // passwords are not the same
			return new ResponseEntity<String>(HttpStatus.PRECONDITION_FAILED);
		}
		if (!user.getEmail().equals(email)) {  // emails are not the same
			return new ResponseEntity<String>(HttpStatus.NOT_FOUND);
		}
		String hashedPassword;
		try {
			hashedPassword = encryptionService.hash(password, email);
		} catch (EncryptionException e) {
			LOG.error("Failed hashing password!!!!  This should NOT happen!");
			return new ResponseEntity<String>(HttpStatus.INTERNAL_SERVER_ERROR);
		}
		user.setPassword(hashedPassword);
		userService.save(user);
		return new ResponseEntity<String>(HttpStatus.OK);
	}	
}
